import java.util.ArrayList;
import java.util.HashMap;

public class BoardMoveGenerator {

	private Board board;
	private boolean testingCheck = false;

	public BoardMoveGenerator(Board board) {
		setBoard(board);
	}

	public BoardMoveGenerator(Board board, boolean testingCheck) {
		setBoard(board);
		setTestingCheck(testingCheck);
	}

	public PieceMoves getPossibleMoves(Piece piece){
		//if piece is the king
		if(piece.getKind() == Piece.TYPE_KING) return getKingMoves(piece);
		//if piece is the queen
		else if(piece.getKind() == Piece.TYPE_QUEEN) return getQueenMoves(piece);
		//if piece is a bishop
		else if(piece.getKind() == Piece.TYPE_BISHOP) return getBishopMoves(piece);
		//if piece is a knight
		else if(piece.getKind() == Piece.TYPE_KNIGHT) return getKnightMoves(piece);
		//if piece is a rook
		else if(piece.getKind() == Piece.TYPE_ROOK) return getRookMoves(piece);
		//if piece is a pawn
		else return getPawnMoves(piece, getBoard().getLastMove());
	}

	private PieceMoves getKingMoves(Piece king){
		BoardCell kingCell = board.getCellWithPieceOn(king);
		PieceMoves moves = new PieceMoves(king, board);
		
		for (int i = -1; i <= 1; i++) {
			for (int j = -1; j <= 1; j++) {
				if(i == 0 && j == 0) continue;
				else addCellIfPossibleKingMove(moves, kingCell.getFile() + i, kingCell.getRank() + j);
			}
		}
		
		// if king is in check, can't castle
		if(board.getKingAttackers() == null || board.getKingAttackers().isEmpty()){
			Piece rookLeft = board.getPieceOn(new BoardCell(Board.MIN_FILE, king.getRank()));
			Piece rookRight = board.getPieceOn(new BoardCell(Board.MAX_FILE, king.getRank()));
			
			testCastleWithRook(rookLeft, moves, -2);
			testCastleWithRook(rookRight, moves, +2);
		}
		
		moves = removeInvalidKingMoves(moves);
		return moves;
	}
	
	// will remove the invalid moves (the ones that would make the king go in check position)
	// from the possible moves array
	private PieceMoves removeInvalidKingMoves(PieceMoves moves) {
		ArrayList<BoardMove> duplicate = new ArrayList<BoardMove>(moves.getAllMoves());
		
		for (BoardCell boardCell : board.getKingAttackers().keySet()) {
			for (BoardMove boardMove : duplicate) {
				BoardCell target = new BoardCell(boardMove.getToFile(), boardMove.getToRank());
				if(board.getPiecesBetween(boardCell, target).size() < 2){
					moves.remove(boardMove);
				}
			}
		}
		
		for (Piece p : board.getOpponentPieces()) {
			PieceMoves pMoves = board.getPieceMoves().get(p);
			if(pMoves != null){
				for (BoardMove attacked : pMoves.getAllMoves()) {
					moves.remove(attacked);
				}
			}
		}
		
		return moves;
	}

	private void testCastleWithRook(Piece rook, PieceMoves moves, int fileAdj) {
		Piece king = moves.getPiece();
		if(rook != null){
			if(!rook.hasMoved() && !king.hasMoved()){
				if(board.hasPathBetween(board.getCellWithPieceOn(king),
						board.getCellWithPieceOn(rook))){
					BoardCell target = new BoardCell(king.getFile() + fileAdj, king.getRank());
					if(board.hasPieceOn(target)){						
						if(board.getKingAttackers().isEmpty() || board.getAttackers(target).isEmpty()){						
							moves.getMoves().add(new BoardMove(target.getFile(), target.getRank()));
						}
					}
				}
			}
		}
	}

	private void addCellIfPossibleKingMove(PieceMoves moves, int file, int rank){
		if(!isTestingCheck()){
			try{
				BoardCell target = new BoardCell(file, rank);
				if(board.hasPieceOn(target)){
					if(board.getPieceOn(target).getColor() == moves.getPiece().getColor()) return;
				}
				
				HashMap<BoardCell, Piece> attackingCells = board.getAttackersOrMovers(target, this, true);
				if(attackingCells.isEmpty()){
					if(!board.hasPieceOn(target)){
						moves.getMoves().add(new BoardMove(target.getFile(), target.getRank()));
					}else{
						moves.getAttacks().add(new BoardMove(target.getFile(), target.getRank()));
					}
				}
			// if a cell outside the board has been tried to be instantiated
			}catch(RuntimeException e){ }
		}
	}

	private PieceMoves getQueenMoves(Piece queen){
		BoardCell queenCell = board.getCellWithPieceOn(queen);
		PieceMoves moves = new PieceMoves(queen, board);

		testDiagonalMoves(moves, queenCell);
		testLineMoves(moves, queenCell);

		return moves;
	}

	private PieceMoves getBishopMoves(Piece bishop){
		BoardCell bishopCell = board.getCellWithPieceOn(bishop);
		PieceMoves moves = new PieceMoves(bishop, board);

		testDiagonalMoves(moves, bishopCell);

		return moves;
	}

	private PieceMoves getKnightMoves(Piece knight){
		BoardCell knightCell = board.getCellWithPieceOn(knight);
		PieceMoves moves = new PieceMoves(knight, board);
		
		int[] fileAdders = { -1, -2, +1, +2};
		int[] rankAdders = { -2, -1, +2, +1};
		
		for (int i = 0; i < fileAdders.length; i++) {
			for (int j = 0; j < rankAdders.length; j++) {
				int fileAdder = fileAdders[i];
				int rankAdder = rankAdders[j];
				if(Math.abs(fileAdder) + Math.abs(rankAdder) == 3){
					addCellIfPossibleMove(moves, knightCell.getFile()+fileAdder, knightCell.getRank()+rankAdder);
				}
			}
		}

		return moves;
	}

	private PieceMoves getRookMoves(Piece rook){
		BoardCell rookCell = board.getCellWithPieceOn(rook);
		PieceMoves moves = new PieceMoves(rook, board);

		testLineMoves(moves, rookCell);

		return moves;
	}

	private PieceMoves getPawnMoves(Piece pawn, Move lastMove){
		BoardCell pawnCell = board.getCellWithPieceOn(pawn);
		int colorAdj = pawn.getColor() == Piece.COLOR_WHITE ? 1 : -1;

		PieceMoves moves = new PieceMoves(pawn, board);

		BoardCell cellToMove = new BoardCell(pawnCell.getFile(), pawnCell.getRank()+(colorAdj*1));
		addCellIfPossibleMove(moves, cellToMove.getFile(), cellToMove.getRank());

		if(!pawn.hasMoved()){
			// cannot make the two-cell jump if there is a piece in the way
			if(!board.hasPieceOn(new BoardCell(pawnCell.getFile(), pawnCell.getRank()+(colorAdj*1)))){
				addCellIfPossibleMove(moves, pawnCell.getFile(), pawnCell.getRank()+(colorAdj*2));
			}
		}
		
		addPawnAttackableCell(moves, pawnCell.getFile()-1, pawn.getRank()+(colorAdj*1));
		addPawnAttackableCell(moves, pawnCell.getFile()+1, pawn.getRank()+(colorAdj*1));
		
		if(canEnPassant(pawn, colorAdj, 1, lastMove)){
			moves.getAttacks().add(new BoardMove(new BoardCell(pawn.getFile()+1, pawn.getRank()+colorAdj)));
		}
		
		if(canEnPassant(pawn, colorAdj, -1, lastMove)){
			moves.getAttacks().add(new BoardMove(new BoardCell(pawn.getFile()-1, pawn.getRank()+colorAdj)));
		}

		return moves;
	}

	private void addPawnAttackableCell(PieceMoves moves, int file, int rank) {
		Piece pawn = moves.getPiece();		
		try{
			BoardCell cell = new BoardCell(file, rank);
			Piece target = board.getPieceOn(cell);
			
			if(target != null){
				if(target.getColor() != pawn.getColor()){
					HashMap<BoardCell, Piece> kingAttackers = board.getKingAttackers();
					if(kingAttackers.isEmpty() || kingAttackers.containsKey(cell)){							
						moves.getAttacks().add(new BoardMove(cell));
					}
				}
			}
			
			// if a pawn has the possibility to capture a piece, it has to do it
			if(moves.getAttacks().size() > 0){
				moves.getMoves().clear();
			}
			// if a cell outside the board has been tried to be instantiated
		}catch(RuntimeException e){ }
	}
	
	private boolean canEnPassant(Piece pawn, int rankIncrementByColor, int side, Move lastMove) {
		boolean canEnPassant = false;
		
		if(lastMove == null) return false;
		
		int rankToBe;
		// pawn must be in fifth rank (counting from its starting point depending on the color)
		if(pawn.getColor() == Piece.COLOR_WHITE) rankToBe = 5;
		else rankToBe = 4;
		
		if(pawn.getRank() == rankToBe){
			try{
				// determine if there is an adjacent piece to this pawn
				Piece pawnBySide = board.getPieceOn(new BoardCell(pawn.getFile()+side, pawn.getRank()));
				// if this piece is also a pawn
				if(pawnBySide.getKind() == Piece.TYPE_PAWN){
					// if this pawn is not the same color as the en passant tried pawn
					if(pawnBySide.getColor() != pawn.getColor()){
						// if the last move was the pawn's move and not any other piece's move
						if(lastMove.getToFile() == pawnBySide.getFile()
								&& lastMove.getToRank() == pawnBySide.getRank()
								&& lastMove.getFromFile() == pawnBySide.getFile()
								&& lastMove.getFromRank() == (pawnBySide.getRank()-(rankIncrementByColor*2))){							
							return true;
						}
					}
				}
			}catch(RuntimeException re){
				return false;
			}
		}
		
		return canEnPassant;
	}
	
	private void testDiagonalMoves(PieceMoves moves, BoardCell cell) {
		int origFile = cell.getFile();
		int origRank = cell.getRank();
		
		for (int i = -1; i <= 1; i=i+2) {
			for (int j = -1; j <= 1; j=j+2) {
				int file = origFile + i;
				int rank = origRank + j;
				
				while(file >= Board.MIN_FILE && file <= Board.MAX_FILE
						&& rank >= Board.MIN_RANK && rank <= Board.MAX_RANK){
					BoardCell target = new BoardCell(file, rank);
					if(board.hasPathBetween(cell, target)){				
						addCellIfPossibleMove(moves, file, rank);
					}
					file = file +i;
					rank = rank +j;
				}
			}
			
		}
	}

	private void testLineMoves(PieceMoves moves, BoardCell cell) {
		for (int rank = Board.MIN_RANK; rank <= Board.MAX_RANK; rank++) {
			BoardCell target = new BoardCell(cell.getFile(), rank);
			if(board.hasPathBetween(cell, target)){				
				addCellIfPossibleMove(moves, cell.getFile(), rank);
			}
		}

		for (int file = Board.MIN_FILE; file <= Board.MAX_FILE; file++) {
			BoardCell target = new BoardCell(file, cell.getRank());
			if(board.hasPathBetween(cell, target)){				
				addCellIfPossibleMove(moves, file, cell.getRank());
			}
		}
	}

	private void addCellIfPossibleMove(PieceMoves pieceMoves, int file, int rank) {
		try{
			BoardCell cell = new BoardCell(file, rank);
			Piece piece = pieceMoves.getPiece();
			Piece target = board.getPieceOn(cell);
			HashMap<BoardCell, Piece> kingAttackers = board.getKingAttackers();
			if(kingAttackers.isEmpty()){
				if(target == null){
					HashMap<BoardCell, Piece> attackers = board.getAttackers(new BoardCell(piece.getFile(), piece.getRank()));
					if(attackers.isEmpty() || piece.getKind() == Piece.TYPE_KING) pieceMoves.getMoves().add(new BoardMove(cell));
					else{
						BoardCell kingCell = board.getKingCell(piece.getColor());
						for (BoardCell oppAttacker : attackers.keySet()) {
							if(board.getPiecesBetween(oppAttacker, kingCell).size() != 1){
								
								if(piece.getKind() == Piece.TYPE_PAWN &&
									(cell.getRank() == Board.MAX_RANK || cell.getRank() == Board.MIN_RANK)){
									// Testing promotion - if possible, pawns can be promoted to every piece excepting a king
									pieceMoves.getMoves().add(new BoardMove(cell, 'Q'));
									pieceMoves.getMoves().add(new BoardMove(cell, 'R'));
									pieceMoves.getMoves().add(new BoardMove(cell, 'N'));
									pieceMoves.getMoves().add(new BoardMove(cell, 'B'));
								}else{
									pieceMoves.getMoves().add(new BoardMove(cell));
								}
							}else{
								// the piece I'm moving is the only one blocking the attacker
								Piece attackerPiece = attackers.get(oppAttacker);
								if(attackerPiece.getKind() == Piece.TYPE_ROOK
										|| attackerPiece.getKind() == Piece.TYPE_BISHOP
										|| attackerPiece.getKind() == Piece.TYPE_QUEEN){
									pieceMoves.getMoves().clear();
									if(!pieceMoves.getAttacks().contains(oppAttacker)){										
										pieceMoves.getAttacks().clear();
										break;
									}
								}
							}
						}
					}
				}else{
					// if piece is pawn, it does not attack the same way as it moves.
					if(piece.getKind() != Piece.TYPE_PAWN){
						if(target.getColor() != piece.getColor()){
							pieceMoves.getAttacks().add(new BoardMove(cell));
						}
					}
				}
			}else{
				if(piece.getKind() != Piece.TYPE_PAWN){					
					if(kingAttackers.containsKey(cell)){
						pieceMoves.getAttacks().add(new BoardMove(cell));
					}else if(board.getCellsToBlock().contains(cell)){
						pieceMoves.getMoves().add(new BoardMove(cell));
					}
				}
			}
		// if a cell outside the board has been tried to be instantiated
		}catch(RuntimeException e){ }
	}

	public Board getBoard() {
		return board;
	}

	public void setBoard(Board board) {
		this.board = board;
	}

	public boolean isTestingCheck() {
		return testingCheck;
	}

	public void setTestingCheck(boolean testingCheck) {
		this.testingCheck = testingCheck;
	}
}
